import type { AutomationOptions, IBrowserManager } from '@shared/types/'
import type { Browser, BrowserContext, Page } from 'playwright'
import { Buffer as NodeBuffer } from 'node:buffer'
import { existsSync } from 'node:fs'
import { appendFile, mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { generateRandomFingerprint } from '@shared/utils/fingerprint'
import { app as electronApp } from 'electron'
import { getAppDataPath, getConfig } from '../config/index'
import {
  createContextLogger,
  generateTraceId,
  log,
  PerformanceLogger,
} from '../logger/index'
import { ProxyLease } from '../shared/proxy-lease'
import * as limiter from './session-limiter'

// 简易并发限制器（跨本模块所有会话生效）
// 目的：限制预约模块同一进程内的 Playwright 会话并发量，防止资源争用/代理配额耗尽。
// 策略：
// - acquireSessionPermit：小于阈值直接进入；否则进入等待队列，顺序放行。
// - releaseSessionPermit：释放并尝试唤醒队列中的下一个等待者。
let currentSessions = 0
const waitQueue: Array<() => void> = []

async function acquireSessionPermit(): Promise<void> {
  const cfg = getConfig()
  const raw = Number(cfg.proxy?.max_concurrent_sessions ?? 0)
  const limit = Number.isFinite(raw) ? raw : 0
  if (limit <= 0) {
    currentSessions++
    return
  }
  if (currentSessions < limit) {
    currentSessions++
    return
  }
  await new Promise<void>(resolve => waitQueue.push(() => resolve()))
  currentSessions++
}

function releaseSessionPermit(): void {
  currentSessions = Math.max(0, currentSessions - 1)
  const next = waitQueue.shift()
  if (next)
    next()
}

export class BookingBrowserManager implements IBrowserManager {
  // Browser/Context/Page 生命周期：
  // - launch：按配置选择代理、初始化 Chromium、创建 Context + Page、注册事件监听（console/requestfailed/pageerror）。
  // - close：按 Page → Context → Browser 顺序释放资源，并释放并发许可。
  // 注意：生产环境使用打包浏览器（resources/browsers），并通过 PLAYWRIGHT_BROWSERS_PATH 路径控制。
  private browser: Browser | null = null
  private context: BrowserContext | null = null
  private page: Page | null = null
  private logger = createContextLogger({ component: 'BookingBrowserManager' })
  private operationId: string = ''
  private networkLoggingEnabled = false
  // 会话专属运行目录：避免并发下通过全局环境变量冲突
  private runDir: string | null = null
  private currentProxy: { server: string, username?: string, password?: string } | null = null
  // 启停互斥，避免 close/launch 并发导致 newPage-on-closed 报错
  private isLaunching = false
  private isClosing = false
  // 记录最近一次关闭完成的时间，用于 close→launch 节流，避免 Windows 上残留句柄导致 newPage 失败
  private lastCloseAt: number = 0
  // 流量统计（按步骤与总量）
  private currentStepLabel: string = 'init'
  private traffic = {
    totalUp: 0,
    totalDown: 0,
    perStep: new Map<string, { up: number, down: number, count: number }>(),
  }

  private async sleep(ms: number): Promise<void> {
    await new Promise<void>(r => setTimeout(r, ms))
  }

  async launch(options?: AutomationOptions & { disableProxy?: boolean }): Promise<void> {
    this.operationId = generateTraceId()

    // 会话并发限制（per_session），避免内存/句柄耗尽或代理池超配。
    await acquireSessionPermit()
    await limiter.acquire()

    try {
      // 若正处于关闭中，等待其完成，避免竞争
      const waitDeadline = Date.now() + 2000
      while (this.isClosing && Date.now() < waitDeadline)
        await this.sleep(50)
      // 若刚执行过 close，给浏览器进程一个缓冲时间，避免 newPage: Target ... closed
      if (this.lastCloseAt > 0) {
        const since = Date.now() - this.lastCloseAt
        const need = 500 - since
        if (need > 0)
          await this.sleep(need)
      }
      this.isLaunching = true

      const config = getConfig()
      PerformanceLogger.start(`booking-browser-launch-${this.operationId}`)

      // 代理选择（方案A）：尊重全局配置；当 disableProxy=true 时强制直连。
      const proxyCandidate = options?.disableProxy ? null : selectProxyForSession(config)
      this.currentProxy = proxyCandidate
      // 代理选择日志（包含黑名单数量与掩码后的认证信息）。
      try {
        const proxyEnabled = !!config?.proxy?.enabled
        const blacklist = Array.isArray(config?.proxy?.temp_blacklist) ? config.proxy.temp_blacklist : []
        this.logger.info('🧩 代理选择', {
          enabled: proxyEnabled,
          blacklist_active: blacklist.filter((b: any) => b && (!b.until || b.until > Date.now())).length,
          picked: proxyCandidate ? maskProxyForLog(proxyCandidate) : null,
        })
      }
      catch {}

      this.logger.info('🚀 启动用于预约的浏览器', {
        operationId: this.operationId,
        headless: options?.headless ?? true,
        timeout: options?.timeout ?? 30000,
        proxy: proxyCandidate ? { server: proxyCandidate.server, hasAuth: !!proxyCandidate.username } : undefined,
      })

      // 打包环境：明确指定 Playwright 浏览器路径（避免回落到用户目录 ms-playwright）。
      let executablePath: string | undefined
      const __isPackaged = (() => {
        try { return !!electronApp?.isPackaged }
        catch { return !!(process as any)?.resourcesPath }
      })()
      if (__isPackaged) {
        if (!process.env.PLAYWRIGHT_BROWSERS_PATH) {
          const maybe = join(process.resourcesPath || process.cwd(), 'browsers')
          process.env.PLAYWRIGHT_BROWSERS_PATH = maybe
        }
        if (!process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD)
          process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = '1'
        const base = join(process.resourcesPath || process.cwd(), 'browsers')
        const candidates = [
          join(base, 'chromium-1193', 'chrome-win', 'chrome.exe'),
          join(base, 'chromium_headless_shell-1193', 'chrome-win', 'headless_shell.exe'),
        ]
        const found = candidates.find(p => existsSync(p))
        if (found)
          executablePath = found
      }

      const { chromium } = await import('playwright')

      this.browser = await chromium.launch({
        headless: options?.headless ?? true,
        timeout: options?.timeout ?? 30000,
        args: [
          '--no-sandbox',
          '--disable-setuid-sandbox',
          '--disable-dev-shm-usage',
          '--disable-accelerated-2d-canvas',
          '--no-first-run',
          '--no-zygote',
          '--disable-gpu',
        ],
        proxy: proxyCandidate ? { server: proxyCandidate.server, username: proxyCandidate.username, password: proxyCandidate.password } : undefined,
        executablePath,
      })

      const fp = generateRandomFingerprint()
      const contextOptions: Parameters<typeof this.browser.newContext>[0] = {
        userAgent: options?.user_agent ? options.user_agent : fp.userAgent,
        viewport: options?.viewport || { width: 1366, height: 768 },
        locale: 'zh-CN',
        timezoneId: 'Asia/Hong_Kong',
        extraHTTPHeaders: {
          'Accept-Language': fp.acceptLanguage,
          'Upgrade-Insecure-Requests': '1',
          ...(fp.secChUa ? { 'sec-ch-ua': fp.secChUa } : {}),
          ...(fp.secChUaMobile ? { 'sec-ch-ua-mobile': fp.secChUaMobile } : {}),
          ...(fp.secChUaPlatform ? { 'sec-ch-ua-platform': fp.secChUaPlatform } : {}),
        },
      }
      this.context = await this.browser.newContext(contextOptions)

      // 降低自动化特征：可配置启用
      try {
        const stealth = !!(config as any)?.network?.stealth
        if (stealth) {
          await this.context.addInitScript(() => {
            Object.defineProperty(navigator, 'webdriver', { get: () => undefined })
          })
        }
      }
      catch {}

      try {
        this.page = await this.context.newPage()
      }
      catch (e) {
        const msg = e instanceof Error ? e.message : String(e)
        // 在 Windows 上，close→launch 很近时偶发：Target page/context/browser has been closed
        if (/Target page, context or browser has been closed/i.test(msg)) {
          this.logger.warn('newPage 失败，重建 Context 后重试', { operationId: this.operationId })
          try { await this.context?.close() }
          catch {}
          this.context = await this.browser.newContext(contextOptions)
          this.page = await this.context.newPage()
        }
        else {
          throw e
        }
      }
      this.page.setDefaultTimeout(options?.timeout ?? 30000)
      // 开启网络监听用于流量统计与网络日志
      try { this.enableNetworkLogging() }
      catch {}

      // 资源拦截（可配置，默认关闭，尽量贴近 automation 模块行为）
      try {
        const blockResources = !!(config as any)?.network?.block_resources
        if (blockResources) {
          await this.context.route('**/*', (route) => {
            const req = route.request()
            const type = req.resourceType()
            const url = req.url()
            if (['image', 'media', 'font', 'manifest', 'beacon', 'eventsource', 'websocket'].includes(type))
              return route.abort()
            if (/\.(?:png|jpe?g|gif|webp|svg|ico)(?:\?|$)/i.test(url))
              return route.abort()
            if (/google-analytics|googletagmanager|doubleclick\.net|hotjar|facebook|optimizely|mixpanel|segment|clarity\.ms|stats|tracker/i.test(url))
              return route.abort()
            return route.continue()
          })
        }
      }
      catch {}

      // 关键事件：
      // - console error：页面抛错时记录便于排查。
      // - requestfailed：网络错误（如 ERR_CONNECTION_RESET）直接输出 URL/方法/错误文本。
      // - pageerror：未捕获异常。
      this.page.on('console', (msg) => {
        if (msg.type() === 'error') {
          const text = msg.text() || ''
          // 对静态资源加载失败噪声统一降级（包括 404 / net::ERR_FAILED / Failed to load resource）
          if (/Failed to load resource/i.test(text) || /net::ERR_/i.test(text) || /404|Not Found/i.test(text)) {
            // this.logger.debug('页面静态资源加载失败', { operationId: this.operationId, message: text })
            return
          }
          this.logger.warn('页面控制台错误', { operationId: this.operationId, message: text })
        }
      })
      this.page.on('requestfailed', (req) => {
        try {
          const type = req.resourceType()
          const url = req.url()
          const failure = req.failure()?.errorText
          const isStatic = ['image', 'media', 'font', 'manifest', 'beacon', 'eventsource', 'websocket'].includes(type)
            || /\.(?:png|jpe?g|gif|webp|svg|ico)(?:\?|$)/i.test(url)
            || /google-analytics|googletagmanager|doubleclick\.net|hotjar|facebook|optimizely|mixpanel|segment|clarity\.ms|stats|tracker/i.test(url)

          // 暂时屏蔽静态资源请求失败日志
          return

          if (isStatic) {
            this.logger.debug('请求失败(非关键/已拦截)', {
              operationId: this.operationId,
              url,
              method: req.method(),
              resourceType: type,
              failure,
            })
            return
          }

          this.logger.warn('请求失败', {
            operationId: this.operationId,
            url,
            method: req.method(),
            resourceType: type,
            failure,
          })
        }
        catch {}
      })
      this.page.on('pageerror', (error) => {
        try {
          const msg = (error && (error as any).message) ? (error as any).message : String(error)
          // 过滤已知的非致命噪声错误，减少日志干扰
          const ignoreList = [
            /Cannot read properties of null \(reading 'style'\)/i,
            /ResizeObserver loop limit exceeded/i,
            /Script error\.?/i,
          ]
          if (ignoreList.some(r => r.test(msg))) {
            this.logger.debug('页面错误(已忽略)', { operationId: this.operationId, message: msg, url: (() => {
              try { return this.page?.url() }
              catch { return undefined }
            })() })
            return
          }
          this.logger.warn('页面错误', { operationId: this.operationId, error: msg, url: (() => {
            try { return this.page?.url() }
            catch { return undefined }
          })() })
        }
        catch {
          this.logger.warn('页面错误', { operationId: this.operationId, error: String(error) })
        }
      })

      const launchDuration = PerformanceLogger.end(`booking-browser-launch-${this.operationId}`)
      log.performance('browser-launch', launchDuration, { operationId: this.operationId, component: 'BookingBrowserManager' })
      this.logger.info('🌐 浏览器启动成功（预约）', {
        operationId: this.operationId,
        duration: launchDuration,
        durationText: `${launchDuration}ms`,
        proxy: proxyCandidate ? maskProxyForLog(proxyCandidate) : undefined,
      })
    }
    catch (error) {
      // 失败要释放并发许可，避免死锁。
      releaseSessionPermit()
      limiter.release()
      const launchDuration = PerformanceLogger.measure(`booking-browser-launch-${this.operationId}`) || 0
      if (error instanceof Error) {
        log.system.error(error, 'BookingBrowserManager.launch', {
          operationId: this.operationId,
          user_action: '预约流程尝试启动浏览器',
          recovery_suggestion: '检查代理与浏览器安装，稍后重试',
        })
      }
      else {
        this.logger.error('浏览器启动失败（预约）', { operationId: this.operationId, error: String(error), duration: launchDuration, durationText: `${launchDuration}ms` })
      }
      throw error
    }
    finally {
      this.isLaunching = false
    }
  }

  async close(): Promise<void> {
    const closeOperationId = generateTraceId()
    PerformanceLogger.start(`booking-browser-close-${closeOperationId}`)

    try {
      // 若正处于启动中，等待其完成
      const waitDeadline = Date.now() + 2000
      while (this.isLaunching && Date.now() < waitDeadline)
        await this.sleep(50)
      this.isClosing = true

      this.logger.info('🔄 开始关闭浏览器（预约）', { operationId: closeOperationId })

      if (this.page) {
        await this.page.close()
        this.page = null
        this.logger.debug('页面已关闭', { operationId: closeOperationId })
      }
      if (this.context) {
        await this.context.close()
        this.context = null
        this.logger.debug('浏览器上下文已关闭', { operationId: closeOperationId })
      }
      if (this.browser) {
        await this.browser.close()
        this.browser = null
        this.logger.debug('浏览器实例已关闭', { operationId: closeOperationId })
      }

      const closeDuration = PerformanceLogger.end(`booking-browser-close-${closeOperationId}`)
      log.performance('browser-close', closeDuration, { operationId: closeOperationId, component: 'BookingBrowserManager' })
      this.logger.info('🔒 浏览器已关闭（预约）', { operationId: closeOperationId, duration: closeDuration, durationText: `${closeDuration}ms` })
      // 释放代理租约
      try {
        if (this.currentProxy?.server)
          ProxyLease.release(this.currentProxy.server)
      }
      catch {}
      // 给底层进程一个极短的喘息，避免紧接着 launch 仍在回收中
      await this.sleep(120)
    }
    catch (error) {
      const closeDuration = PerformanceLogger.measure(`booking-browser-close-${closeOperationId}`) || 0
      const msg = error instanceof Error ? error.message : String(error)
      // 已关闭/目标不存在，降级为 warn，不再抛出，避免影响后续重启
      if (/Target page, context or browser has been closed|Target closed|Page closed/i.test(msg)) {
        this.logger.warn('关闭浏览器时目标已关闭（忽略）', { operationId: closeOperationId, reason: msg, duration: closeDuration, durationText: `${closeDuration}ms` })
      }
      else {
        if (error instanceof Error) {
          log.system.error(error, 'BookingBrowserManager.close', {
            operationId: closeOperationId,
            user_action: '预约流程尝试关闭浏览器',
            recovery_suggestion: '忽略此错误，浏览器资源可能已被释放',
          })
        }
        else {
          this.logger.error('关闭浏览器失败（预约）', { operationId: closeOperationId, error: String(error), duration: closeDuration, durationText: `${closeDuration}ms` })
        }
      }
    }
    finally {
      // 释放并发许可。
      releaseSessionPermit()
      limiter.release()
      this.isClosing = false
      this.lastCloseAt = Date.now()
    }
  }

  async newPage(): Promise<Page> {
    if (!this.context)
      throw new Error('浏览器上下文未初始化')
    const page = await this.context.newPage()
    return page
  }

  getCurrentPage(): Page | null {
    return this.page
  }

  /**
   * 设置本会话专属的快照目录（绝对路径）。
   * 如果设置，则本实例的所有 HTML/截图/网络日志都会写入该目录。
   */
  public setRunDirectory(dir: string): void {
    this.runDir = String(dir || '') || null
  }

  /**
   * 将当前使用的代理加入临时黑名单（按配置TTL），并返回 true 表示已加入或无需加入。
   */
  banCurrentProxy(reason?: string): boolean {
    try {
      const cfg: any = getConfig()
      if (!this.currentProxy || !cfg?.proxy?.enabled)
        return true
      const ttlMin: number = Math.max(1, Number(cfg?.proxy?.blacklist_ttl_minutes || 60))
      const entry = { server: this.currentProxy.server, until: Date.now() + ttlMin * 60_000, reason }
      if (!Array.isArray(cfg.proxy.temp_blacklist))
        cfg.proxy.temp_blacklist = []
      cfg.proxy.temp_blacklist.push(entry)
      this.logger.warn('当前代理已加入黑名单', { server: this.currentProxy.server, ttlMinutes: ttlMin, reason })
      return true
    }
    catch {
      return false
    }
  }

  /**
   * 启用网络请求/响应日志，写入当前运行目录的 network-log.jsonl。
   * 需在设置好 BOOKING_SNAPSHOT_DIR（或调用引擎初始化 run 目录）之后调用。
   */
  enableNetworkLogging(): void {
    if (!this.page)
      return
    if (this.networkLoggingEnabled)
      return
    this.networkLoggingEnabled = true
    const getNetLogPath = async (): Promise<string> => {
      const dir = await this.getSnapshotsPath()
      try { await mkdir(dir, { recursive: true }) }
      catch {}
      return join(dir, 'network-log.jsonl')
    }
    try { this.logger.info('network logging enabled') }
    catch {}

    const onReq = (req: any) => {
      const type = req.resourceType?.() || 'other'
      if (!['document', 'xhr', 'fetch'].includes(type))
        return
      void (async () => {
        try {
          const p = await getNetLogPath()
          const rec = {
            t: Date.now(),
            type: 'request',
            method: req.method(),
            url: req.url(),
            resourceType: req.resourceType(),
            headers: req.headers(),
          }
          await appendFile(p, `${JSON.stringify(rec)}\n`, 'utf8')
          // 统计上行字节：请求头 + 请求体
          try {
            const headersObj = req.headers() || {}
            const headersSize = NodeBuffer.byteLength(JSON.stringify(headersObj), 'utf8')
            let bodySize = 0
            try {
              const pd = req.postData()
              if (typeof pd === 'string')
                bodySize = NodeBuffer.byteLength(pd, 'utf8')
            }
            catch {}
            const up = headersSize + bodySize
            this.addTraffic(up, 0, 1)
            // 调试：打印前几条请求的估算字节
            try {
              if ((this as any)._dbgReqsCount === undefined)
                (this as any)._dbgReqsCount = 0
              if ((this as any)._dbgReqsCount < 5) {
                (this as any)._dbgReqsCount++
                this.logger.info('traffic+req', { step: this.currentStepLabel, up, url: req.url() })
              }
            }
            catch {}
          }
          catch {}
        }
        catch {}
      })()
    }
    const onRes = (res: any) => {
      const req = res.request?.() as any
      const type = req?.resourceType?.() || 'other'
      if (!['document', 'xhr', 'fetch'].includes(type))
        return
      void (async () => {
        try {
          const p = await getNetLogPath()
          const headers = await res.headers()
          const rec = {
            t: Date.now(),
            type: 'response',
            url: res.url(),
            status: res.status(),
            headers,
          }
          await appendFile(p, `${JSON.stringify(rec)}\n`, 'utf8')
          // 统计下行字节：响应头 + 内容长度（尽量不读取 body，避免性能开销）
          try {
            const headersSize = NodeBuffer.byteLength(JSON.stringify(headers || {}), 'utf8')
            const cl = Number((headers?.['content-length'] as any) || (headers?.['Content-Length'] as any) || 0)
            const down = headersSize + (Number.isFinite(cl) ? Math.max(0, cl) : 0)
            this.addTraffic(0, down, 0)
            // 调试：打印前几条响应的估算字节
            try {
              if ((this as any)._dbgResCount === undefined)
                (this as any)._dbgResCount = 0
              if ((this as any)._dbgResCount < 5) {
                (this as any)._dbgResCount++
                this.logger.info('traffic+res', { step: this.currentStepLabel, down, url: res.url(), status: res.status() })
              }
            }
            catch {}
          }
          catch {}
        }
        catch {}
      })()
    }
    try {
      this.page.on('request', onReq)
      this.page.on('response', onRes)
      // 避免重复统计：仅挂 Page 级监听
    }
    catch {}
  }

  async saveHtml(filename: string, html?: string): Promise<string> {
    // 保存到 booking-run-snapshots（开发写入项目根目录；生产写入 AppData）。
    const saveOperationId = generateTraceId()
    PerformanceLogger.start(`booking-html-save-${saveOperationId}`)
    try {
      const tempPath = await this.getSnapshotsPath()
      try { await mkdir(tempPath, { recursive: true }) }
      catch {}
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
      const fullFilename = `${timestamp}_${filename}.html`
      const filePath = join(tempPath, fullFilename)

      this.logger.debug('保存HTML（预约）', { operationId: saveOperationId, filename: fullFilename, hasProvidedContent: !!html })

      let content = html
      if (!content && this.page)
        content = await this.page.content()

      if (content) {
        await writeFile(filePath, content, 'utf-8')
        const saveDuration = PerformanceLogger.end(`booking-html-save-${saveOperationId}`)
        this.logger.debug('HTML保存成功（预约）', { operationId: saveOperationId, filePath, contentSize: `${(content.length / 1024).toFixed(2)}KB`, duration: saveDuration, durationText: `${saveDuration}ms` })
        return filePath
      }
      throw new Error('无内容可保存')
    }
    catch (error) {
      const saveDuration = PerformanceLogger.measure(`booking-html-save-${saveOperationId}`) || 0
      if (error instanceof Error) {
        log.system.error(error, 'BookingBrowserManager.saveHtml', { operationId: saveOperationId, filename, user_action: '预约流程保存HTML', recovery_suggestion: '检查磁盘空间和文件权限' })
      }
      else {
        this.logger.error('保存HTML失败（预约）', { operationId: saveOperationId, filename, error: String(error), duration: saveDuration, durationText: `${saveDuration}ms` })
      }
      throw error
    }
  }

  async screenshot(filename?: string): Promise<string> {
    if (!this.page)
      throw new Error('页面未初始化')

    const screenshotOperationId = generateTraceId()
    PerformanceLogger.start(`booking-screenshot-${screenshotOperationId}`)
    try {
      const tempPath = await this.getSnapshotsPath()
      try { await mkdir(tempPath, { recursive: true }) }
      catch {}
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
      const fullFilename = filename || `screenshot_${timestamp}.png`
      const filePath = join(tempPath, fullFilename)
      this.logger.debug('截图（预约）', { operationId: screenshotOperationId, filename: fullFilename, fullPage: true })
      await this.page.screenshot({ path: filePath, fullPage: true })
      const screenshotDuration = PerformanceLogger.end(`booking-screenshot-${screenshotOperationId}`)
      this.logger.debug('截图已保存（预约）', { operationId: screenshotOperationId, filePath, duration: screenshotDuration, durationText: `${screenshotDuration}ms` })
      return filePath
    }
    catch (error) {
      const screenshotDuration = PerformanceLogger.measure(`booking-screenshot-${screenshotOperationId}`) || 0
      if (error instanceof Error) {
        log.system.error(error, 'BookingBrowserManager.screenshot', { operationId: screenshotOperationId, filename, user_action: '预约流程截图', recovery_suggestion: '检查页面状态和磁盘空间' })
      }
      else {
        this.logger.error('截图失败（预约）', { operationId: screenshotOperationId, filename, error: String(error), duration: screenshotDuration, durationText: `${screenshotDuration}ms` })
      }
      throw error
    }
  }

  /**
   * 保存综合快照：根据配置保存 HTML + PNG，并写入页面信息 JSON。
   * 文件名统一为 `${timestamp}_${name}.{html|png|json}`，保存在当前运行目录。
   */
  async saveSnapshot(name: string): Promise<{ htmlPath?: string, screenshotPath?: string, infoPath?: string }> {
    if (!this.page)
      throw new Error('页面未初始化')
    const opId = generateTraceId()
    PerformanceLogger.start(`booking-save-snapshot-${opId}`)
    const cfg = getConfig() as any
    const saveHtmlEnabled = !!(cfg?.logging?.save_html)
    const savePngEnabled = !!(cfg?.logging?.save_snapshots)
    try {
      const dir = await this.getSnapshotsPath()
      try { await mkdir(dir, { recursive: true }) }
      catch {}
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
      const base = join(dir, `${timestamp}_${name}`)
      let htmlPath: string | undefined
      let shotPath: string | undefined
      if (saveHtmlEnabled) {
        try {
          const html = await this.page.content()
          htmlPath = `${base}.html`
          await writeFile(htmlPath, html, 'utf8')
        }
        catch {}
      }
      if (savePngEnabled) {
        try {
          shotPath = `${base}.png`
          await this.page.screenshot({ path: shotPath, fullPage: true })
        }
        catch {}
      }
      // 页面信息
      const info: any = { url: '', title: '', at: new Date().toISOString(), viewport: null as any, userAgent: '' }
      try { info.url = this.page.url() }
      catch {}
      try { info.title = await this.page.title() }
      catch {}
      try { info.viewport = this.page.viewportSize?.() || null }
      catch {}
      try { info.userAgent = await this.page.evaluate(() => navigator.userAgent) }
      catch {}
      const infoPath = `${base}.json`
      try { await writeFile(infoPath, JSON.stringify(info, null, 2), 'utf8') }
      catch {}
      const dur = PerformanceLogger.end(`booking-save-snapshot-${opId}`)
      this.logger.debug('综合快照保存完成（预约）', { operationId: opId, name, duration: dur, durationText: `${dur}ms`, htmlPath, screenshotPath: shotPath, infoPath })
      return { htmlPath, screenshotPath: shotPath, infoPath }
    }
    catch (error) {
      const dur = PerformanceLogger.measure(`booking-save-snapshot-${opId}`) || 0
      if (error instanceof Error) {
        log.system.error(error, 'BookingBrowserManager.saveSnapshot', { operationId: opId, name, duration: dur })
      }
      else {
        this.logger.error('综合快照保存失败（预约）', { operationId: opId, name, error: String(error), duration: dur })
      }
      return {}
    }
  }

  // 统一获取当前实例应写入的快照目录
  private async getSnapshotsPath(): Promise<string> {
    if (this.runDir && this.runDir.length > 0)
      return this.runDir
    return await getBookingSnapshotsPath()
  }

  // ============== 流量统计辅助 ==============
  public setCurrentStepLabel(label: string): void {
    this.currentStepLabel = label || 'unknown'
  }

  private addTraffic(upBytes: number, downBytes: number, incCount = 0): void {
    if (upBytes > 0)
      this.traffic.totalUp += upBytes
    if (downBytes > 0)
      this.traffic.totalDown += downBytes
    const key = this.currentStepLabel || 'unknown'
    const cur = this.traffic.perStep.get(key) || { up: 0, down: 0, count: 0 }
    cur.up += Math.max(0, upBytes)
    cur.down += Math.max(0, downBytes)
    if (incCount > 0)
      cur.count += 1
    this.traffic.perStep.set(key, cur)
  }

  public getTrafficSnapshot(): { totalUp: number, totalDown: number, perStep: Record<string, { up: number, down: number, count: number }> } {
    const per: Record<string, { up: number, down: number, count: number }> = {}
    for (const [k, v] of this.traffic.perStep.entries()) per[k] = { ...v }
    // 调试输出，便于定位 0B 的情况
    try {
      this.logger.info('traffic snapshot', { totalUp: this.traffic.totalUp, totalDown: this.traffic.totalDown, steps: Object.keys(per).length })
    }
    catch {}
    return { totalUp: this.traffic.totalUp, totalDown: this.traffic.totalDown, perStep: per }
  }
}

let bookingBrowserManager: BookingBrowserManager | null = null
export function getBrowserManager(): BookingBrowserManager {
  if (!bookingBrowserManager)
    bookingBrowserManager = new BookingBrowserManager()
  return bookingBrowserManager
}

// 代理选择（方案A）
// 输入：全局配置（proxy.enabled/providers/pool/temp_blacklist）。
// 输出：{ server, username?, password? } 或 null。
// 策略：合并顶层与各 provider 的 pool，应用黑名单过滤后随机挑选。
function selectProxyForSession(cfg: ReturnType<typeof getConfig> | any): { server: string, username?: string, password?: string } | null {
  try {
    const proxy = (cfg && cfg.proxy) ? cfg.proxy : null
    if (!proxy || proxy.enabled !== true)
      return null

    const blacklist: Array<{ server: string, until?: number }> = Array.isArray(proxy.temp_blacklist) ? proxy.temp_blacklist : []
    const now = Date.now()

    const candidates: Array<{ host: string, port: string, username?: string, password?: string, serverStr: string }> = []

    function pushEntry(entry: string, fallbackAuth?: { username: string, password: string }, proto?: string) {
      const parts = String(entry || '').split(':')
      if (parts.length >= 4) {
        const host = parts[0]
        const port = parts[1]
        const username = parts[2]
        const password = parts.slice(3).join(':')
        const serverStr = `${(proto || 'http')}://${host}:${port}`
        candidates.push({ host, port, username, password, serverStr })
      }
      else if (parts.length >= 2) {
        const host = parts[0]
        const port = parts[1]
        const username = fallbackAuth?.username
        const password = fallbackAuth?.password
        const serverStr = `${(proto || 'http')}://${host}:${port}`
        candidates.push({ host, port, username, password, serverStr })
      }
    }

    const providers: any[] = Array.isArray(proxy.providers) ? proxy.providers : []
    for (const p of providers) {
      if (!p || p.enabled === false)
        continue
      const pProto = p.protocol || 'http'
      const pAuth = (p.auth && p.auth.username) ? p.auth : undefined
      const pool: string[] = Array.isArray(p.pool) ? p.pool : []
      for (const e of pool) pushEntry(e, pAuth, pProto)
    }

    if (candidates.length === 0)
      return null

    const filtered = candidates.filter((c) => {
      for (const b of blacklist) {
        if (!b || (b.until && b.until <= now))
          continue
        if (b.server === c.serverStr)
          return false
        if (b.server && b.server.endsWith(`${c.host}:${c.port}`))
          return false
      }
      return true
    })

    const pool = filtered.length > 0 ? filtered : candidates
    // 代理租约：优先选未被租约占用的 server，选中后加租约，避免并发复用同一出口
    const ttlSec: number = Math.max(30, Number((cfg?.proxy as any)?.lease_ttl_seconds || 300))
    for (let i = 0; i < pool.length; i++) {
      const idx = (Math.floor(Math.random() * pool.length) + i) % pool.length
      const tryPick = pool[idx]
      if (ProxyLease.lease(tryPick.serverStr, ttlSec * 1000))
        return { server: tryPick.serverStr, username: tryPick.username, password: tryPick.password }
    }
    // 若都被占用，仍返回一个（不加租约），避免阻塞；上层可根据失败再重试
    const fallback = pool[Math.floor(Math.random() * pool.length)]
    return { server: fallback.serverStr, username: fallback.username, password: fallback.password }
  }
  catch {
    return null
  }
}

function maskProxyForLog(p: { server: string, username?: string, password?: string }) {
  // 掩码日志输出中的认证信息（仅保留用户名，密码以 *** 隐去）。
  if (!p)
    return undefined
  if (!p.username)
    return { server: p.server }
  const u = p.username
  const masked = p.password ? `${u}:***` : u
  return { server: p.server, auth: masked }
}

// ===============================
// 快照路径（预约专用）
// - 开发：项目根目录 booking-run-snapshots
// - 生产：应用数据目录 AppData/booking-run-snapshots
// 支持通过 BOOKING_SNAPSHOT_DIR 覆盖
// ===============================
async function getBookingSnapshotsPath(): Promise<string> {
  const envDir = process.env.BOOKING_SNAPSHOT_DIR
  if (envDir && typeof envDir === 'string' && envDir.length > 0)
    return envDir
  const isDev = process.env.NODE_ENV !== 'production'
  if (isDev)
    return join(process.cwd(), 'booking-run-snapshots')
  return join(getAppDataPath(), 'booking-run-snapshots')
}
